<?php

namespace Desktopd\MUA;


// POP3Connection (Mail Receiver)
class POP3Connection implements Debuggable {
    const STATE_AUTHORIZATION = 1;
    const STATE_TRANSACTION = 2;
    const STATE_UPDATE = 3;
    
    
    protected $connection;
    protected $state = self::STATE_AUTHORIZATION;
    protected $apop = false;
    protected $count = false;
    protected $totalSize = false;
    protected $deleted = array();
    
    protected $debugHandlers = array();
    
    // available features
    protected $STARTTLS = false;
    protected $minDelay = false;
    
    
    public function __construct (TCPClientConnection $connection) {
        $this->connection = $connection;
        if (!$connection->isNew()) {
            // Something already happened
            throw new \RuntimeException('A fresh connection is needed!');
        }
        
    }
    
    public function isState ($state) {
        return $state === $this->state;
    }
    
    public function isConnected () {
        return $this->connection->isConnected();
    }
    
    public function isTLS () {
        return $this->connection->isTLS();
    }
    
    public function addDebugListener (/*callable*/ $callback) {
        if (!is_callable($callback)) {
            return false;
        }
        
        if (!in_array($callback, $this->debugHandlers, true)) {
            $this->debugHandlers[] = $callback;
        }
        
        return true;
    }
    
    public function removeDebugListener (/*callable*/ $callback) {
        foreach (array_keys($this->debugHandlers, $callback, true) as $key) {
            unset($this->debugHandlers[$key]);
        }
        
        $this->debugHandlers = array_values($this->debugHandlers);
        
        return true;
    }
    
    protected function callDebugger ($data = '') {
        $count = 0;
        foreach ($this->debugHandlers as $callback) {
            if (!is_callable($callback)) continue;
            $callback("$data");
            $count++;
        }
        
        return $count;
    }
    
    protected function writeLine ($line = '') {
        if (!$this->isConnected()) {
            return false;
        }
        
        $line = str_replace(array("\r", "\n"), '', "$line");
        
        $this->callDebugger("> $line\r\n");
        
        return $this->connection->write("$line\r\n");
    }
    
    // read a one-line response
    // and returns *trimmed* response
    protected function readResponseLine ($raw = false) {
        if (!$this->isConnected()) {
            return false;
        }
        
        $line = $this->connection->readLine();
        if (!$line) {
            return false;
        }
        
        $this->callDebugger("< $line");
        
        $trimmed = trim($line);
        
        if ($raw) {
            return $trimmed;
        }
        
        $status = explode(' ', $trimmed, 2);
        if ($status[0] !== '+OK') {
            return false;
        }
        
        return isset($status[1]) ? $status[1] : ''; // use '=== false' to find failures
    }
    
    // read data after the '+OK' line
    protected function readMultilineResponse ($returnBroken = false) {
        $data = '';
        while (true) {
            $line = $this->connection->readLine();
            if (false === $line) {
                return $returnBroken ? $data : false;
            }
            
            $this->callDebugger("< $line");
            
            if ($line === ".\r\n" || $line === ".\n" || $line === ".") {
                break;
            }
            
            if ($line[0] === '.') {
                $line = substr($line, 1);
            }
            
            $data .= $line;
        }
        
        return $data;
    }
    
    public function readGreetings () {
        $greetings = $this->readResponseLine();
        if (false === $greetings) {
            return false;
        }
        
        if (preg_match('~(<.+>)$~', $greetings, $matches)) {
            $this->apop = $matches[1];
        } else {
            $this->apop = false;
        }
        
        return true;
    }
    
    public function getCapabilities () {
        if (!$this->writeLine("CAPA")) {
            return false;
        }
        
        if (false === $this->readResponseLine()) {
            return false;
        }
        
        $lines = $this->readMultilineResponse();
        if (false === $lines) {
            return false;
        }
        
        $lines = explode("\n", trim($lines));
        foreach ($lines as $line) {
            $tokens = explode(' ', trim($line));
            $name = array_shift($tokens);
            switch ($name) {
                case 'STLS':
                    $this->STARTTLS = true;
                    break;
                
                case 'LOGIN-DELAY':
                    $this->minDelay = isset($tokens[0]) ? $tokens[0] : '';
                    break;
                
            }
        }
        
        return true;
    }
    
    public function getMinimumDelay () {
        return $this->minDelay;
    }
    
    public function startTLS () {
        if (!$this->isState(self::STATE_AUTHORIZATION)) {
            return false;
        }
        
        if ($this->connection->isTLS()) {
            // TLS enabled
            return false;
        }
        
        if (!$this->STARTTLS) {
            return false; // not supported
        }
        
        if (!$this->writeLine('STLS')) {
            return false;
        }
        
        if (false === $this->readResponseLine()) {
            return false;
        }
        
        if (!$this->connection->startTLS()) {
            return false;
        }
        
        $this->STARTTLS = false;
        
        /*
        if (!$this->readGreetings()) {
            return false;
        }
        */
        
        return true;
    }
    
    public function authenticate ($user, $password) {
        if (!$this->isState(self::STATE_AUTHORIZATION)) {
            return false;
        }
        
        $user = trim("$user");
        
        if ($this->apop) {
            // APOP
            
            $digest = md5("{$this->apop}{$password}");
            if (!$this->writeLine("APOP $user $digest")) {
                return false;
            }
            
            if (false === $this->readResponseLine()) {
                return false;
            }
            
            // success
            
        } else {
            // USER / PASS
            
            if (!$this->writeLine("USER $user")) {
                return false;
            }
            
            if (false === $this->readResponseLine()) {
                return false;
            }
            
            if (!$this->writeLine("PASS $password")) {
                return false;
            }
            
            if (false === $this->readResponseLine()) {
                return false;
            }
            
            // success
            
        }
        
        $this->state = self::STATE_TRANSACTION;
        return true;
    }
    
    public function stat () {
        if (!$this->isState(self::STATE_TRANSACTION)) {
            return false;
        }
        
        if (!$this->writeLine('STAT')) {
            return false;
        }
        
        $result = $this->readResponseLine();
        if (false === $result) {
            return false;
        }
        
        list($count, $totalSize) = explode(' ', $result);
        if (is_numeric($count)) {
            $this->count = round($count);
        }
        
        if (is_numeric($totalSize)) {
            $this->totalSize = round($totalSize);
        }
        
        return true;
    }
    
    public function countMessages () {
        if (false === $this->count) {
            $this->stat();
        }
        
        return $this->count;
    }
    
    public function getTotalSize () {
        return $this->totalSize;
    }
    
    protected function checkMessageNumber ($n) {
        $n = trim("$n");
        
        if (!is_numeric($n)) {
            return false;
        }
        
        $n = round($n);
        if ($n < 1) {
            return false;
        }
        
        $count = $this->countMessages();
        if (false === $count) {
            return false;
        }
        
        if ($n > $count) {
            return false;
        }
        
        if (in_array($n, $this->deleted, true)) {
            return false;
        }
        
        return $n;
    }
    
    public function retrieveMessage ($n) {
        if (!$this->isState(self::STATE_TRANSACTION)) {
            return false;
        }
        
        $n = $this->checkMessageNumber($n);
        if (false === $n) {
            return false;
        }
        
        if (!$this->writeLine("RETR $n")) {
            return false;
        }
        
        if (false === $this->readResponseLine()) {
            return false;
        }
        
        return $this->readMultilineResponse();
    }
    
    public function deleteMessage ($n) {
        if (!$this->isState(self::STATE_TRANSACTION)) {
            return false;
        }
        
        $n = $this->checkMessageNumber($n);
        if (false === $n) {
            return false;
        }
        
        if (!$this->writeLine("DELE $n")) {
            return false;
        }
        
        if (false === $this->readResponseLine()) {
            return false;
        }
        
        $this->deleted[] = $n;
        
        return true;
    }
    
    public function noop () {
        if (!$this->isState(self::STATE_TRANSACTION)) {
            return false;
        }
        
        if (!$this->writeLine('NOOP')) {
            return false;
        }
        
        return false !== $this->readResponseLine();
    }
    
    public function reset () {
        if (!$this->isState(self::STATE_TRANSACTION)) {
            return false;
        }
        
        if (!$this->writeLine('RSET')) {
            return false;
        }
        
        if (false === $this->readResponseLine()) {
            return false;
        }
        
        $this->deleted = array();
        
        return true;
    }
    
    public function countDeleted () {
        return count($this->deleted);
    }
    
    public function quit () {
        if (!$this->isState(self::STATE_TRANSACTION)) {
            return false;
        }
        
        if (!$this->writeLine('QUIT')) {
            return false;
        }
        
        $response = $this->readResponseLine();
        // should close the connection
        $this->state = self::STATE_UPDATE;
        
        return false !== $response;
    }
}


// vim: ts=4 et ai

